package dslab.core;

import dslab.enums.Protocol;
import dslab.enums.Role;
import dslab.mailbox.MailboxDMAPHandler;
import dslab.mailbox.MailboxDMTPHandler;
import dslab.nameserver.INameserverRemote;
import dslab.transfer.TransferDMTPHandler;
import dslab.util.Config;
import dslab.util.Message;
import dslab.util.Pair;
import dslab.util.WriteTask;

import java.io.IOException;
import java.net.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;

/**
 * (TCP) server class representing the base for all DMTP/DMAP server implementations;
 *
 * has different constructors for transfer and mailbox contexts;
 *
 * implements the runnable and shutdownable interfaces, server can be multi-threaded and
 * controlled this way
 */
public class Server implements Runnable, Shutdownable {

    private final String componentId;
    private final int port;
    private final Role role;
    private final Protocol protocol;
    private final ExecutorService pool;

    // exclusively transfer related members
    private final Config domains;
    private final BlockingQueue<Runnable> outboundSendTasks;
    private final Pair<InetAddress, Integer> monitoring;
    private final INameserverRemote root;

    // exclusively mailbox related members
    private final String domain;
    private final Config users;
    private final BlockingQueue<WriteTask> writeTasks;
    private final Map<String, List<Message>> mails;

    private ServerSocket serverSocket;

    /**
     * Server constructor meant to be called from a TransferServer context; uninitialized members represent
     * variables that are ignored in a TransferServer context
     * @param componentId string representation of this component
     * @param port TCP port to listen on
     * @param role context in which this server will be used
     * @param protocol protocol this server has to serve
     * @param domains domains configuration for look-ups
     * @param pool thread pool to launch new client connection threads with
     * @param outboundSendTasks queue of send tasks that will be delegated to SendQueueHandler
     */
    public Server(String componentId, int port, Role role, Protocol protocol, Config domains,
                  ExecutorService pool, BlockingQueue<Runnable> outboundSendTasks,
                  Pair<InetAddress, Integer> monitoring, INameserverRemote root) {
        assert role == Role.Transfer;

        // members used in this context
        this.componentId = componentId;
        this.port = port;
        this.role = role;
        this.protocol = protocol;
        this.domains = domains;
        this.pool = pool;
        this.outboundSendTasks = outboundSendTasks;
        this.monitoring = monitoring;
        this.root = root;

        // members not used in this context
        this.domain = null;
        this.users = null;
        this.writeTasks = null;
        this.mails = null;
    }

    /**
     * Server constructor meant to be called from a MailboxServer context; uninitialized members represent
     * variables that are ignored in a MailboxServer context
     * @param componentId string representation of this component
     * @param port TCP port to listen on
     * @param role context in which this server will be used
     * @param protocol protocol this server has to serve
     * @param users user configuration for look-ups
     * @param pool thread pool to launch new client connection threads with
     * @param writeTasks queue of write tasks that will be delegated to WriteQueueHandler
     * @param mails reference to map with mapping of users to lists of messages
     */
    public Server(String componentId, String domain, int port, Role role, Protocol protocol,
                  Config users, ExecutorService pool, BlockingQueue<WriteTask> writeTasks,
                  Map<String, List<Message>> mails) {
        assert role == Role.Mailbox;

        // members used in this context
        this.componentId = componentId;
        this.port = port;
        this.domain = domain;
        this.role = role;
        this.protocol = protocol;
        this.users = users;
        this.pool = pool;
        this.writeTasks = writeTasks;
        this.mails = mails;

        // members not used in this context
        this.domains = null;
        this.outboundSendTasks = null;
        this.monitoring = null;
        this.root = null;
    }

    @Override
    public void run() {
        try {
            System.out.printf("@%s: server with role %s and protocol %s started%n", componentId, role, protocol);
            // bind server socket to port and wait on connections
            serverSocket = new ServerSocket(port);

            int id = 0;
            while (true) {
                // accept incoming connection and create thread to delegate
                // handling of client connection
                Socket clientConnection = serverSocket.accept();
                /* TODO: refactor enums into interfaces */
                switch (role) {
                    // if this server assumes a TransferServer role
                    case Transfer:
                        // launch new client connection thread that handles the incoming connection
                        switch (protocol) {
                            // using a transfer DMTP handler
                            case DMTP:
                                pool.execute(
                                    new TransferDMTPHandler(
                                        String.format("%s-handler-%d", componentId, ++id),
                                        clientConnection,
                                        domains,
                                        outboundSendTasks,
                                        monitoring,
                                        root
                                    )
                                );
                                break;
                            // other TransferServer configurations are either not implemented or meant to be processed
                            default:
                                System.err.printf(
                                    "@%s: unimplemented protocol %s configured server in role %s%n",
                                    componentId,
                                    protocol,
                                    role
                                );
                                break;
                        }
                       break;
                    // if this server assumes a MailboxServer role
                    case Mailbox:
                        // launch new client connection thread that handles the incoming connection
                        switch (protocol) {
                            // using a mailbox DMTP handler
                            case DMTP:
                                pool.execute(
                                    new MailboxDMTPHandler(
                                        String.format("%s-handler-%d", componentId, ++id),
                                        clientConnection,
                                        domain,
                                        users,
                                        writeTasks,
                                        mails
                                    )
                                );
                                break;
                            // using a mailbox DMAP handler
                            case DMAP:
                                pool.execute(
                                    new MailboxDMAPHandler(
                                        String.format("%s-handler-%d", componentId, ++id),
                                        clientConnection,
                                        users,
                                        writeTasks,
                                        mails
                                    )
                                );
                                break;
                            // other MailboxServer configurations are either not implemented or meant to be processed
                            default:
                                System.err.printf(
                                    "@%s: unimplemented protocol %s configured server in role %s%n",
                                    componentId,
                                    protocol,
                                    role
                                );
                                break;
                        }
                       break;
                    // other Server configurations are either not implemented or meant to be processed
                    default:
                        System.err.printf("@%s: unimplemented role %s configured server%n", componentId, role);
                        break;
                }
           }
        } catch (BindException exception) {
            System.err.printf(
                "@%s: a fatal error has occurred: can't bind to port %d: %s%n",
                componentId,
                port,
                exception.getMessage()
            );
        } catch (SocketException exception) {
            System.out.printf("@%s: server was closed%n", componentId);
        } catch (IOException exception) {
            System.err.printf("@%s: a fatal io error has occurred%n", componentId);
            exception.printStackTrace();
        } finally {
            // attempt closing server
            if (serverSocket != null && !serverSocket.isClosed()) {
                try {
                    serverSocket.close();
                } catch (IOException exception) {
                    System.err.printf(
                        "@%s: a fatal error has occurred while closing the socket with port %d%n",
                        componentId,
                        port
                    );
                    exception.printStackTrace();
                }
            }
        }
    }

    @Override
    public void shutdown() {
        try {
            serverSocket.close();
        } catch (IOException ignored) {}
    }
}
